Skip to content

fix(mock): emit bare factory call for primitive-union $ref in additionalProperties (#3200) - #3504

Merged
melloware merged 3 commits into
orval-labs:masterfrom
wadakatu:fix/mock-additionalproperties-primitive-union-ref-3200
Jun 1, 2026
Merged

fix(mock): emit bare factory call for primitive-union $ref in additionalProperties (#3200)#3504
melloware merged 3 commits into
orval-labs:masterfrom
wadakatu:fix/mock-additionalproperties-primitive-union-ref-3200

Conversation

@wadakatu

@wadakatu wadakatu commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

What

Fixes invalid TypeScript generated for an additionalProperties dictionary whose value is a $ref to a primitive oneOf/anyOf (e.g. IntegerLike = number | string) when per-schema faker factories are enabled (schemas: true).

Given:

IntegerLike:
  oneOf:
    - type: integer
    - type: string
StringToIntegerMap:
  type: object
  additionalProperties:
    $ref: '#/components/schemas/IntegerLike'

orval emitted:

export const getStringToIntegerMapMock = (): StringToIntegerMap => ({
  [faker.string.alphanumeric(5)]: { ...getIntegerLikeMock() }, // ❌
});

getIntegerLikeMock() returns a primitive union (number | string), which is not spreadable, so the output fails to compile with TS2698: Spread types may only be created from object types and would discard the value as {} at runtime. After the fix the dictionary value is the bare call:

[faker.string.alphanumeric(5)]: getIntegerLikeMock(), // ✅

Why

When delegating a $ref to its get<X>Mock() factory, the code wrapped the call in { ...get<X>Mock() } whenever the schema had oneOf/anyOf — without checking what the branches actually resolve to. A composition of primitives is not object-like, so it must not be spread.

The delegation now treats a oneOf/anyOf as object-like only when every branch resolves to an object (compositionResolvesToObject). Object compositions (e.g. Dog | Cat) keep the spread form; primitive unions emit the bare call. The type === 'object' and allOf arms are unchanged, so existing fixtures (e.g. petstore's Pet/Dog) are byte-for-byte identical — no snapshot changes.

Note on the original report

The issue was filed on 8.6.2 describing an IntegerLike | undefined widening. That exact symptom no longer reproduces on master (faker v10's arrayElement<const T>(): T no longer returns undefined, and the Partial<>/overrideResponse spread is no longer emitted for dictionary factories). The remaining type error is the TS2698 above, which surfaces through the schemas: true faker factory path added later in #3426. It hits the same schema shape, so this PR addresses it under #3200.

Test plan

  • New spec tests/specifications/issue-3200.yaml (covers both oneOf and anyOf primitive-union dictionary values) and issue3200 config in mock.config.ts (faker, schemas: true, operationResponses: true).
  • Focused regression test in api-generation.spec.ts asserting the bare get<X>Mock() call and the absence of { ...get<X>Mock() }.
  • The type-level guarantee is enforced by scripts/typecheck-generated.mjs (failed with TS2698 before, passes after).
  • bun run test, bun run test:snapshots, bun run typecheck, bun run lint, bun run format:check all pass; existing snapshots unchanged.

Closes #3200

Summary by CodeRabbit

  • Bug Fixes

    • Prevented invalid mock output when generating dictionary/additionalProperties values that are primitive unions by emitting direct factory calls instead of spreading union results.
  • Tests

    • Added regression tests and generated fixtures validating correct mock emission for dictionary schemas with primitive-union values.

…nalProperties (orval-labs#3200)

When `schemas: true` emits per-schema faker factories, an `additionalProperties`
dictionary whose value is a $ref to a primitive `oneOf`/`anyOf` (e.g. `number |
string`) delegated to `get<X>Mock()` but wrapped the call in `{ ...get<X>Mock()
}`. The factory returns a primitive union, which is not spreadable: the output
failed to compile (TS2698) and would discard the value as `{}` at runtime.

The delegation now treats a `oneOf`/`anyOf` as object-like only when every
branch resolves to an object, so primitive unions emit the bare `get<X>Mock()`
call while object compositions keep the spread form.

Closes orval-labs#3200
Copilot AI review requested due to automatic review settings June 1, 2026 09:07
@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7f375067-d046-4427-898f-d23bc1bfc2ab

📥 Commits

Reviewing files that changed from the base of the PR and between e6f6a55 and 58836b2.

📒 Files selected for processing (1)
  • tests/api-generation.spec.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/api-generation.spec.ts

📝 Walkthrough

Walkthrough

Conservatively detect when a referenced schema actually resolves to an object shape before emitting { ...getXMock() }. Add a helper to resolve $ref and composition branches, update the mock resolver to use it, and add an OpenAPI spec, Orval config, generated snapshots, and a Vitest regression for Issue 3200.

Changes

Dictionary Mock Generation Fix

Layer / File(s) Summary
Object-like composition detection
packages/mock/src/faker/resolvers/value.ts
Replace permissive oneOf/anyOf handling with resolvesToObjectLike(...) that resolves $refs, recognizes object indicators (type: 'object', properties, additionalProperties, allOf), and requires all composition branches to resolve to objects with cycle guards.
Spec, config, and generated snapshots
tests/specifications/issue-3200.yaml, tests/configs/mock.config.ts, tests/__snapshots__/mock/issue-3200/*
Add OpenAPI reproduction spec, Orval mock generation config entry, and generated snapshot artifacts (model types, faker mocks, endpoints) that produce dictionary mocks whose values are bare primitive-union factory calls.
Regression test
tests/api-generation.spec.ts
Add Vitest regression verifying additionalProperties dictionary values referencing primitive unions are emitted as bare getIntegerLikeMock()/getNumberLikeMock() calls and not spread-wrapped.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant resolveMockValue
  participant resolvesToObjectLike
  participant getRefInfo
  Client->>resolveMockValue: request mock generation for additionalProperties $ref
  resolveMockValue->>resolvesToObjectLike: is schema object-like?
  resolvesToObjectLike->>getRefInfo: resolve $ref -> schema
  resolvesToObjectLike->>resolvesToObjectLike: recursively validate oneOf/anyOf branches
  resolvesToObjectLike-->>resolveMockValue: boolean result
  resolveMockValue-->>Client: emit bare factory call or object spread accordingly
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Possibly related PRs

  • orval-labs/orval#3431: Both PRs modify packages/mock/src/faker/resolvers/value.ts—related changes to handling composed schemas in mock generation.

Suggested labels

mock, msw, bug

Suggested reviewers

  • melloware

Poem

🐰 I hopped through schemas, refs, and code-ink,
Found unions that tried to masquerade as a link,
I chased each $ref down every branching trail,
Only true objects now get spread — the rest stay pale,
Hooray — maps no longer hide undefined in the tale!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main fix: emitting bare factory calls for primitive-union $refs in additionalProperties, directly addressing the core issue.
Linked Issues check ✅ Passed The PR fully addresses issue #3200 by fixing the invalid TypeScript emission and undefined widening through conservative object-like detection in the resolvesToObjectLike logic.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the primitive-union additionalProperties mock generation issue; no unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds a regression test and generator logic to fix invalid spreading of primitive-union faker mock factories when used as additionalProperties dictionary values under schemas: true (Issue #3200).

Changes:

  • Added an OpenAPI reproduction spec and test config entry for Issue 3200.
  • Added a regression test asserting dictionary values use a bare get<X>Mock() call (no object spread).
  • Updated faker mock value resolver to only use spread delegation when the referenced schema resolves to an object-like shape; added corresponding snapshots.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/specifications/issue-3200.yaml New OpenAPI spec reproducing the primitive-union $ref dictionary case.
tests/configs/mock.config.ts Adds generation config for Issue 3200 outputs (schemas + operation response mocks).
tests/api-generation.spec.ts Adds regression assertion to prevent { ...get<X>Mock() } for primitive unions.
tests/snapshots/mock/issue-3200/model/stringToNumberMap.ts New snapshot for generated dictionary interface.
tests/snapshots/mock/issue-3200/model/stringToIntegerMap.ts New snapshot for generated dictionary interface.
tests/snapshots/mock/issue-3200/model/numberLike.ts New snapshot for generated primitive union type.
tests/snapshots/mock/issue-3200/model/integerLike.ts New snapshot for generated primitive union type.
tests/snapshots/mock/issue-3200/model/index.ts New snapshot barrel export for Issue 3200 models.
tests/snapshots/mock/issue-3200/model/index.faker.ts New snapshot for faker factories ensuring bare call delegation.
tests/snapshots/mock/issue-3200/endpoints.ts New snapshot for generated endpoints and response mocks.
packages/mock/src/faker/resolvers/value.ts Fixes delegation logic by checking whether compositions resolve to object before spreading; adds helper.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +464 to +468
function compositionResolvesToObject(
schema: MockSchema,
context: ContextSpec,
seen = new Set<string>(),
): boolean {
Comment on lines +469 to +490
let resolved: Partial<OpenApiSchemaObject> | undefined =
schema as Partial<OpenApiSchemaObject>;

if (isReference(schema)) {
const refPath = typeof schema.$ref === 'string' ? schema.$ref : '';
if (seen.has(refPath)) {
return false;
}
seen.add(refPath);
const { refPaths } = getRefInfo(refPath, context);
resolved = Array.isArray(refPaths)
? (prop(
context.spec,
// @ts-expect-error: refPaths are not guaranteed to be valid keys of the spec
...refPaths,
) as Partial<OpenApiSchemaObject>)
: undefined;
}

if (!resolved) {
return false;
}
Comment on lines +472 to +477
if (isReference(schema)) {
const refPath = typeof schema.$ref === 'string' ? schema.$ref : '';
if (seen.has(refPath)) {
return false;
}
seen.add(refPath);
Comment thread tests/api-generation.spec.ts Outdated
Comment on lines +813 to +821
expect(content).toContain(
'[faker.string.alphanumeric(5)]: getIntegerLikeMock(),',
);
expect(content).toContain(
'[faker.string.alphanumeric(5)]: getNumberLikeMock(),',
);
// The primitive-union factory call must never be spread into the object.
expect(content).not.toContain('{ ...getIntegerLikeMock() }');
expect(content).not.toContain('{ ...getNumberLikeMock() }');

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/mock/src/faker/resolvers/value.ts`:
- Around line 467-477: The bug is that the shared Set parameter seen is being
mutated and reused across sibling branches, causing false cycles; fix by
treating seen as a recursion stack: do not mutate the original Set for sibling
branches—when you add a reference (refPath) or recurse into a oneOf/anyOf
branch, create a new Set (e.g., clone seen and add refPath) or push/pop so each
recursive path gets its own stack; update all usages around isReference handling
and the oneOf/anyOf branch recursion sites so each branch receives its own
copied/isolated seen instead of sharing the same Set.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4f90c64b-da66-4828-b104-95e93724e3f6

📥 Commits

Reviewing files that changed from the base of the PR and between 9f8c350 and 401bbe4.

📒 Files selected for processing (11)
  • packages/mock/src/faker/resolvers/value.ts
  • tests/__snapshots__/mock/issue-3200/endpoints.ts
  • tests/__snapshots__/mock/issue-3200/model/index.faker.ts
  • tests/__snapshots__/mock/issue-3200/model/index.ts
  • tests/__snapshots__/mock/issue-3200/model/integerLike.ts
  • tests/__snapshots__/mock/issue-3200/model/numberLike.ts
  • tests/__snapshots__/mock/issue-3200/model/stringToIntegerMap.ts
  • tests/__snapshots__/mock/issue-3200/model/stringToNumberMap.ts
  • tests/api-generation.spec.ts
  • tests/configs/mock.config.ts
  • tests/specifications/issue-3200.yaml

Comment thread packages/mock/src/faker/resolvers/value.ts Outdated
wadakatu and others added 2 commits June 1, 2026 18:18
…ck (orval-labs#3200)

Addresses review feedback on the additionalProperties dictionary fix:

- The cycle-guard `Set` was shared and mutated across all `oneOf`/`anyOf`
  branches, so the first branch could poison its siblings: a composition like
  `oneOf: [{$ref: Foo}, {$ref: Foo}]` made the second branch look cyclic and
  return `false`, misclassifying an object-only union as non-object-like. The
  guard now takes a fresh copy at each `$ref` hop, so siblings sharing a `$ref`
  no longer trip it.
- Rename `compositionResolvesToObject` -> `resolvesToObjectLike`; it also
  recognizes plain object schemas (`properties`/`additionalProperties`/`allOf`),
  not just compositions.
- Return early when a `$ref` is not a string instead of using an `''` fallback
  key, and initialize `resolved` via an explicit if/else for clarity.
- Make the regression assertions whitespace-tolerant and detect a spread
  regardless of brace formatting.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MSW mock for additionalProperties schema widens dictionary values to include undefined

3 participants